/*
 * Copyright (C) 2011 Wojciech Dzierżanowski
 * See LICENSE.txt for licensing details.
 */

package wdzierzan.downstream.core;

import ch.ethz.ssh2.ChannelCondition;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author Wojciech Dzierzanowski <wojciech.dzierzanowski@gmail.com>
 */
public class SshGstStreamer implements Streamer {
    
    private static final Logger logger = Logger.getLogger(SshGstStreamer.class.getName());

    private static final long COMMAND_OUTPUT_READ_TIMEOUT_MS = 12000;

    private final Connection connection;
    private GstPipelineBuilder pipelineBuilder;
    private GstPipelineBuilder verbatimPipelineBuilder;

    public SshGstStreamer(String hostname) {
        connection = new Connection(hostname);
    }

    SshGstStreamer(Connection connection) {
        this.connection = connection;
    }

    public boolean connect(String username, String password) throws IOException {
        return connect(username, password, 0);
    }

    /**
     * @param username username
     * @param password password
     * @param timeout connection establishment timeout in ms.  <code>0</code>
     *      means try connecting forever
     * @return whether the user was authenticated successfully
     * @throws IOException
     */
    public boolean connect(String username, String password, int timeout) throws IOException {
        connection.connect(null, 0, timeout);
        return connection.authenticateWithPassword(username, password);
//        File pemFile = new File("/home/user/.ssh/id_rsa");
//        connection.authenticateWithPublicKey("user", pemFile, "pass");
    }

    public void disconnect() {
        connection.close();
    }

    public String[] listFiles(String path) throws IOException {
        Session session = connection.openSession();
        try {
            executeCommand("ls -L1p " + PathUtils.escape(path) + '/', session);
            boolean isDirectory = session.getStdout().available() > 0 || session.getStderr().available() == 0;

            session.close();
            session = connection.openSession();
            String command = "ls -L1p " + PathUtils.escape(path);
            executeCommand(command, session);
            boolean isFile = session.getStderr().available() == 0;

            if (isDirectory) {
                StringBuilder stdout = readOutput(session.getStdout());
                logger.log(Level.FINEST, "$ {0}\n{1}", new Object[] {command, stdout});
                return stdout.length() > 0 ? stdout.toString().split("\n") : new String[]{};
            } else if (isFile) {
                return null;
            } else {
                throw new FileNotFoundException(path);
            }
        } finally {
            if (session != null)
                session.close();
        }
    }

    private void executeCommand(String command, Session session) throws IOException {
        session.execCommand(command);
        int reason = session.waitForCondition(ChannelCondition.EOF, COMMAND_OUTPUT_READ_TIMEOUT_MS);
        if ((reason & ChannelCondition.TIMEOUT) != 0)
            throw new IOException("Timed out");
    }

    public void setPipeline(GstPipelineBuilder builder) {
        this.pipelineBuilder = builder;
    }

    public void setVerbatimPipeline(GstPipelineBuilder verbatimBuilder) {
        this.verbatimPipelineBuilder = verbatimBuilder;
    }

    public FileType identify(String path) throws IOException {
        FileType type = FileType.UNSTREAMABLE;
        String fileInfo = getFileInfo(path);
        if (fileInfo != null) {
            if (isLosslessAudio(fileInfo)) {
                logger.log(Level.INFO, "{0} looks like lossless audio to me", path);
                type = FileType.LOSSLESS_AUDIO;
            } else if (isLossyAudio(fileInfo)) {
                logger.log(Level.INFO, "{0} looks like lossy audio to me", path);
                type = FileType.LOSSY_AUDIO;
            } else {
                logger.log(Level.FINER, "{0} looks like nothing particular to me", path);
            }
        }
        return type;
    }

    public void stream(String path, FileType type) throws IOException {

        GstPipelineBuilder bestPipelineBuilder =
                type == FileType.LOSSY_AUDIO && verbatimPipelineBuilder != null
                        ? verbatimPipelineBuilder : pipelineBuilder;

        if (bestPipelineBuilder == null)
            throw new IllegalStateException("Must set the pipeline first");

        Session session = connection.openSession();
        try {
            String pipeline = bestPipelineBuilder.from(path);
            logger.log(Level.INFO, "Pipeline: {0}", pipeline);

            session.execCommand("exec `which gst-launch-0.10` " + pipeline);

            if (logger.isLoggable(Level.FINE))
                logger.fine(readOutput(session.getStdout()).toString());
            if (logger.isLoggable(Level.WARNING))
                logger.warning(readOutput(session.getStderr()).toString());

            int status = -1;
            session.waitForCondition(ChannelCondition.EXIT_STATUS, 2000);
            if (session.getExitStatus() == null) {
                logger.warning("Could not determine command status");
            } else {
                status = session.getExitStatus();
                logger.log(Level.INFO, "Command returned with {0}", status);
            }
            if (status != 0)
                throw new IOException("Command returned with " + status);
        } finally {
            if (session != null)
                session.close();
        }
    }

    private StringBuilder readOutput(InputStream stream) throws IOException {
        StringBuilder output = new StringBuilder();
        InputStreamReader reader = new InputStreamReader(stream);
        for (int i = reader.read(); i != -1; i = reader.read())
            output.append((char) i);
        return output;
    }

    private String getFileInfo(String path) throws IOException {
        Session session = null;
        try {
            String command = "file " + PathUtils.escape(path);
            session = connection.openSession();
            session.execCommand(command);

            StringBuilder output = readOutput(session.getStdout());
            logger.finest(output.toString());

            int infoPos = path.length() + 2;
            return output.length() > infoPos ? output.substring(infoPos) : null;
        } finally {
            if (session != null)
                session.close();
        }
    }

    private static boolean isLossyAudio(String fileInfo) {
        return fileInfo.contains("audio") || fileInfo.contains("Audio")
                || fileInfo.startsWith("MPEG ADTS")
                || fileInfo.startsWith("Microsoft ASF");
    }

    private static boolean isLosslessAudio(String fileInfo) {
        return fileInfo.contains("FLAC audio bitstream")
                || (fileInfo.startsWith("RIFF (little-endian) data, WAVE") && !fileInfo.contains("MPEG"));
    }
}
